from scipy import spatial
from scipy import stats
import numpy as np
class NonparaClassfication(object):

    def __init__(self):
        super(object, self).__init__()
        """mahalanobis, jensenshannon"""
        self.dis_list = ["braycurtis", "canberra", "chebyshev",
                          "cityblock", "correlation", "cosine",
                          "dice", "euclidean", "hamming", "jaccard",
                           "kulsinski",
                          "matching", "minkowski", "rogerstanimoto",
                          "russellrao", "seuclidean", "sokalmichener",
                          "sokalsneath", "sqeuclidean", "yule"]

    def standard_distance(self,all_pattern,simu_resp,class_ideal_response,distance):
        """
        将各类的理想作答与实际作答进行比较，取最短距离的那一类理想作答对应的属性掌握模式作为被试的属性掌握模式
        :param all_pattern: 所有可能的属性掌握模式,type=arr,shape=(可能的属性掌握模式个数c,属性个数k)
        :param simu_resp: 观察到的实际作答，type=arr,shape=(被试个数n,题目个数i)
        :param class_ideal_response: 每个可能的属性掌握模式对应的理想作答，type=arr, shape=(可能的属性掌握模式个数c，题目个数i)
        :param distance: 以什么距离作为度量标准,type=str
        :return: 被试的属性掌握模式,type=arr,shape=(被试个数n,属性个数k)
        """
        # 获取距离矩阵，每一行代表一个被试的实际作答，每一列代表一种掌握模式的理想作答，二者交叉处就是二者的距离
        if distance == "weight_hamming":
            distance_mat = self.__weight_hamming(simu_resp,class_ideal_response)
        else :
            distance_mat = spatial.distance.cdist(simu_resp,class_ideal_response,distance)
        # 在多列中，取出距离最短的列对应的列索引
        result_class = distance_mat.argmin(axis=1)
        # 根据索引在所有可能的掌握模式中取出对应的掌握模式。
        classed_pattern = all_pattern[np.array(result_class)]
        return classed_pattern

    def long_standard_distance(self,t,all_pattern,simu_resp,class_ideal_response,distance,weight=False):
        class_pattern = []
        for t_ in range(t):
            if weight==False:
                class_pattern.append(self.standard_distance(all_pattern=all_pattern,simu_resp=simu_resp[t_,:,:],class_ideal_response=class_ideal_response[t_,:,:],distance=distance))
            else:
                class_pattern.append(self.weighted_standard_distance(all_pattern=all_pattern,simu_resp=simu_resp[t_,:,:],class_ideal_response=class_ideal_response[t_,:,:],distance=distance))

        return np.array(class_pattern)


    def long_standard_dis_constrain_1(self,t,all_pattern,simu_resp,class_ideal_response,distance,return_judge=False,input_first_t_att=None):

        # 给被试分类t次
        class_pattern = []
        for t_ in range(t):
            class_pattern.append(self.standard_distance(all_pattern=all_pattern,simu_resp=simu_resp[t_,:,:],class_ideal_response=class_ideal_response[t_,:,:],distance=distance))
        class_pattern = np.array(class_pattern)

        if input_first_t_att is not None:
            class_pattern[0,:,:] = input_first_t_att

        # 施加特定限制
        judge_list = [[0 for i in range(simu_resp.shape[1])]]
        classfication_result = [class_pattern[0,:,:]]
        for t_ in range(t-1):
            judge = class_pattern[t_,:,:]<=class_pattern[t_+1,:,:]
            judge_list.append(np.product(judge,axis=1))
        for t_ in range(t):
            if t_ >0:
                classfication_result.append(np.nansum(class_pattern[:t_,:,:],axis=0)>=1)
        if return_judge==True:
            return np.array(classfication_result,dtype=np.int8),np.array(judge_list)
        return np.array(classfication_result,dtype=np.int8)

    def long_standard_dis_constrain_2(self,t,all_pattern,simu_resp,class_ideal_response,distance,input_first_t_att=None):

        if distance == "weight_hamming":
            dis_arr = np.array([self.__weight_hamming(simu_resp[t_,:,:], class_ideal_response[t_,:,:]) for t_ in range(t)])
        else:
            dis_arr = np.array([spatial.distance.cdist(simu_resp[t_,:,:], class_ideal_response[t_,:,:], distance) for t_ in range(t)])
        same_dis_mat = []
        for t_ in range(t):
            x = np.min(dis_arr[t_,:,:],axis=1).reshape(-1,1)
            y = dis_arr[t_,:,:]
            _ = x==y
            same_dis_mat.append(_)
        same_dis_mat = np.array(same_dis_mat)
        result_list = []
        result_list.append(self.standard_distance(all_pattern, simu_resp[0,:,:], class_ideal_response[0,:,:], distance))
        if input_first_t_att is not None:
            result_list[0] = input_first_t_att
        for t_ in range(1,t):

            t_th_result_list = []
            for n in range(simu_resp.shape[1]):
                t_1 = result_list[t_-1][n]
                t_2 = same_dis_mat[t_,n,:]
                n_att = self.__select_att_(t_1_att=t_1,t_2_att_arr=t_2,all_pattern=all_pattern)
                t_th_result_list.append(n_att.flatten())
            result_list.append(np.array(t_th_result_list))

        return np.array(result_list)

    def __select_att_(self,t_1_att,t_2_att_arr,all_pattern):
        t_2_att_index=np.arange(len(t_2_att_arr))[t_2_att_arr>0]
        if len(t_2_att_index) <=1:
            return all_pattern[t_2_att_index]
        else:
            judge = np.product(t_1_att<=all_pattern[t_2_att_index],axis=1)
            mat = all_pattern[t_2_att_index][judge==1]
            if len(mat) == 0:
                return all_pattern[np.random.choice(t_2_att_index)]
            return mat[np.random.choice(np.arange(len(mat)))]

    def __get_weight(self,resp_mat):
        correct_response_rate = (np.nanmean(resp_mat,axis=0))
        weight = correct_response_rate*(1-correct_response_rate)
        return weight/(np.nansum(weight))*10


    def __weight_hamming(self,simu_resp,ideal_resp):
        weight = self.__get_weight(resp_mat=simu_resp)
        container = np.zeros((simu_resp.shape[0],ideal_resp.shape[0]))
        for i in range(ideal_resp.shape[0]):
            dif_mat = simu_resp != ideal_resp[i,:]
            weight_dif_mat = dif_mat*weight
            weight_dif_mat_ = weight_dif_mat.sum(axis=1)
            container[:,i] = weight_dif_mat_
        return container

    def weighted_standard_distance(self,all_pattern,simu_resp,class_ideal_response,distance):
        # 获取距离矩阵，每一行代表一个被试的实际作答，每一列代表一种掌握模式的理想作答，二者交叉处就是二者的距离
        distance_mat = self.__weight_hamming(simu_resp,class_ideal_response)
        # 在多列中，取出距离最短的列对应的列索引
        result_class = distance_mat.argmin(axis=1)
        # 根据索引在所有可能的掌握模式中取出对应的掌握模式。
        classed_pattern = all_pattern[np.array(result_class)]
        return classed_pattern